package org.box.metadata.cli.util;

import java.io.PrintWriter;
import java.lang.reflect.Field;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
import java.util.Set;

import org.apache.commons.cli.Options;
import org.box.metadata.cli.annotation.Argument;
import org.box.metadata.cli.annotation.Arguments;
import org.box.metadata.cli.annotation.Command;
import org.box.metadata.cli.annotation.Option;
import org.box.metadata.cli.exception.InvalidConfigurationException;

/**
 * A formatter of help messages for the given command line configuration.
 * 
 * @author <a href="mailto:alexander.box@gmail.com">Alexander Korobka</a>
 */
@SuppressWarnings("rawtypes")
public class HelpFormatter {

	private final org.apache.commons.cli.HelpFormatter delegate = new org.apache.commons.cli.HelpFormatter();
	private final Class[] cfgClasses;
	private boolean convertPropsToOptions = true;
	private boolean incCommonOptions;

	/**
	 * Constructs a new instance of {@link HelpFormatter}
	 * 
	 * @param cfgClasses
	 *            - the class of application's configuration
	 */
	public HelpFormatter(Class... cfgClasses) {
		this.cfgClasses = cfgClasses;
	}

	/**
	 * Generates usage section for help created from several parts: <li>Commands
	 * - generates a list of commands, if those are present in the Configuration
	 * class
	 * 
	 * <li>Options - adds a string [options] to the resulted string, if at least
	 * one options are present in the Configuration.
	 * 
	 * <li>Arguments - If non-Enumerated Arguments field is present in the
	 * Configuration and it has non-empty syntax parameter, it is being returned
	 * otherwise an aggregated string from Enumerated Argument fields
	 * 
	 * @return usage section
	 */
	public String generateCmdLineSyntax() {

		StringBuilder sb = new StringBuilder();
		Set<Command> commands = Util.createCLICommands(cfgClasses);
		if (commands.size() > 0) {
			for (Command cmd : commands) {
				if (sb.length() > 0) {
					sb.append("|");
				}
				sb.append(cmd.fullName());
			}
		}
		sb.append(" " + (hasOptions() ? "[options] " : ""));

		String syntax = getNonEnumeratedArgsSyntax();
		if (syntax == null || "".equals(syntax.trim())) {
			sb.append(getEnumeratedArgsSyntax());
		} else {
			sb.append(syntax);
		}

		return sb.toString();
	};

	/**
	 * Returns true, if cfgClasses has at least one option annotation
	 * 
	 * @return true, if configuration class has options
	 */
	protected boolean hasOptions() {
		for (Class c : cfgClasses) 
			for (Field f : c.getDeclaredFields()) {
				// check non-enumerated arguments
				if (f.isAnnotationPresent(Option.class)) {
					return true;
				}
			}
		return false;
	}

	/**
	 * Generates a syntax string from Enumerated Arguments for help section
	 * 
	 * @return a usage syntax as string
	 */
	protected String getEnumeratedArgsSyntax() {
		List<Argument> args = new ArrayList<Argument>();
		for (Class c : cfgClasses)
			for (Field f : c.getDeclaredFields()) {
				// check enumerated arguments
				if (f.isAnnotationPresent(Argument.class)) {
					args.add(f.getAnnotation(Argument.class));
				}
			}
		
		Collections.sort(args, new Comparator<Argument>() {

			public int compare(Argument o1, Argument o2) {
				return new Integer(o1.index()).compareTo(o2.index());
			}
		});

		StringBuilder sb = new StringBuilder();
		for (Argument a : args) {
			sb.append(a.required() ? a.name() + " " : "[" + a.name() + "] ");
		}

		return sb.toString();
	}

	/**
	 * Generates a syntax string from non-Enumerated Arguments for help section
	 * 
	 * @return a usage syntax as string
	 */
	protected String getNonEnumeratedArgsSyntax() {
		for (Class c : cfgClasses)
			for (Field f : c.getDeclaredFields()) {
				// check non-enumerated arguments
				if (f.isAnnotationPresent(Arguments.class)) {
					Arguments as = f.getAnnotation(Arguments.class);
					return as.syntax();
				}
			}
		return null;
	}

	/**
	 * Print the help for <code>configuration</code> with auto generated command
	 * line syntax. This method prints help information to System.out.
	 * 
	 */
	public void printHelp() {
		printHelp(org.apache.commons.cli.HelpFormatter.DEFAULT_WIDTH,
				generateCmdLineSyntax(), null, null, false);
	}

	/**
	 * Print the help for <code>configuration</code> with auto generated command
	 * line syntax. This method prints help information to the given writer.
	 * 
	 * @param pw
	 *            - the output help print writer
	 */
	public void printHelp(PrintWriter pw) {
		printHelp(pw, org.apache.commons.cli.HelpFormatter.DEFAULT_WIDTH,
				generateCmdLineSyntax(), null,
				org.apache.commons.cli.HelpFormatter.DEFAULT_LEFT_PAD,
				org.apache.commons.cli.HelpFormatter.DEFAULT_DESC_PAD, "");
	}

	/**
	 * Print the help for <code>configuration</code> with the specified command
	 * line syntax. This method prints help information to System.out.
	 * 
	 * @param cmdLineSyntax
	 *            the syntax for this application
	 * @throws InvalidConfigurationException
	 *             if the given class is not compatible or valid as an
	 *             Application's configuration
	 */
	public void printHelp(String cmdLineSyntax)
			throws InvalidConfigurationException {
		printHelp(org.apache.commons.cli.HelpFormatter.DEFAULT_WIDTH,
				cmdLineSyntax, null, null, false);
	}

	/**
	 * Print the help for <code>configuration</code> with the specified command
	 * line syntax. This method prints help information to System.out.
	 * 
	 * @param cmdLineSyntax
	 *            the syntax for this application
	 * @param autoUsage
	 *            whether to print an automatically generated usage statement
	 * @throws InvalidConfigurationException
	 *             if the given class is not compatible or valid as an
	 *             Application's configuration
	 */
	public void printHelp(String cmdLineSyntax, boolean autoUsage)
			throws InvalidConfigurationException {
		printHelp(org.apache.commons.cli.HelpFormatter.DEFAULT_WIDTH,
				cmdLineSyntax, null, null, autoUsage);
	}

	/**
	 * Print the help for <code>configuration</code> with the specified command
	 * line syntax. This method prints help information to System.out.
	 * 
	 * @param cmdLineSyntax
	 *            the syntax for this application
	 * @param header
	 *            the banner to display at the begining of the help
	 * @param footer
	 *            the banner to display at the end of the help
	 * @throws InvalidConfigurationException
	 *             if the given class is not compatible or valid as an
	 *             Application's configuration
	 */
	public void printHelp(String cmdLineSyntax, String header, String footer)
			throws InvalidConfigurationException {
		printHelp(cmdLineSyntax, header, footer, false);
	}

	/**
	 * Print the help for <code>configuration</code> with the specified command
	 * line syntax. This method prints help information to System.out.
	 * 
	 * @param cmdLineSyntax
	 *            the syntax for this application
	 * @param header
	 *            the banner to display at the begining of the help
	 * @param footer
	 *            the banner to display at the end of the help
	 * @param autoUsage
	 *            whether to print an automatically generated usage statement
	 * @throws InvalidConfigurationException
	 *             if the given class is not compatible or valid as an
	 *             Application's configuration
	 */
	public void printHelp(String cmdLineSyntax, String header, String footer,
			boolean autoUsage) throws InvalidConfigurationException {
		printHelp(org.apache.commons.cli.HelpFormatter.DEFAULT_WIDTH,
				cmdLineSyntax, header, footer, autoUsage);
	}

	/**
	 * Print the help for <code>configuration</code> with the specified command
	 * line syntax. This method prints help information to System.out.
	 * 
	 * @param width
	 *            the number of characters to be displayed on each line
	 * @param cmdLineSyntax
	 *            the syntax for this application
	 * @param header
	 *            the banner to display at the beginning of the help
	 * @param footer
	 *            the banner to display at the end of the help
	 * @throws InvalidConfigurationException
	 *             if the given class is not compatible or valid as an
	 *             Application's configuration
	 */
	public void printHelp(int width, String cmdLineSyntax, String header,
			String footer) throws InvalidConfigurationException {
		printHelp(width, cmdLineSyntax, header, footer, false);
	}

	/**
	 * Print the help for <code>configuration</code> with the specified command
	 * line syntax. This method prints help information to System.out.
	 * 
	 * @param width
	 *            the number of characters to be displayed on each line
	 * @param cmdLineSyntax
	 *            the syntax for this application
	 * @param header
	 *            the banner to display at the begining of the help
	 * @param options
	 *            the Options instance
	 * @param footer
	 *            the banner to display at the end of the help
	 * @param autoUsage
	 *            whether to print an automatically generated usage statement
	 * @throws InvalidConfigurationException
	 *             if the given class is not compatible or valid as an
	 *             Application's configuration
	 */
	public void printHelp(int width, String cmdLineSyntax, String header,
			String footer, boolean autoUsage)
			throws InvalidConfigurationException {
		PrintWriter pw = new PrintWriter(System.out);

		printHelp(pw, width, cmdLineSyntax, header,
				org.apache.commons.cli.HelpFormatter.DEFAULT_LEFT_PAD,
				org.apache.commons.cli.HelpFormatter.DEFAULT_DESC_PAD, footer,
				autoUsage);
		pw.flush();
	}

	/**
	 * Print the help for <code>configuration</code> with the specified command
	 * line syntax.
	 * 
	 * @param pw
	 *            the writer to which the help will be written
	 * @param width
	 *            the number of characters to be displayed on each line
	 * @param cmdLineSyntax
	 *            the syntax for this application
	 * @param header
	 *            the banner to display at the begining of the help
	 * @param leftPad
	 *            the number of characters of padding to be prefixed to each
	 *            line
	 * @param descPad
	 *            the number of characters of padding to be prefixed to each
	 *            description line
	 * @param footer
	 *            the banner to display at the end of the help
	 * @throws InvalidConfigurationException
	 *             if the given class is not compatible or valid as an
	 *             Application's configuration
	 * 
	 * @throws IllegalStateException
	 *             if there is no room to print a line
	 */
	public void printHelp(PrintWriter pw, int width, String cmdLineSyntax,
			String header, int leftPad, int descPad, String footer)
			throws InvalidConfigurationException {
		printHelp(pw, width, cmdLineSyntax, header, leftPad, descPad, footer,
				false);
	}

	/**
	 * Print the help for <code>configuration</code> with the specified command
	 * line syntax.
	 * 
	 * @param pw
	 *            the writer to which the help will be written
	 * @param width
	 *            the number of characters to be displayed on each line
	 * @param cmdLineSyntax
	 *            the syntax for this application
	 * @param header
	 *            the banner to display at the begining of the help
	 * @param leftPad
	 *            the number of characters of padding to be prefixed to each
	 *            line
	 * @param descPad
	 *            the number of characters of padding to be prefixed to each
	 *            description line
	 * @param footer
	 *            the banner to display at the end of the help
	 * @param autoUsage
	 *            whether to print an automatically generated usage statement
	 * @throws InvalidConfigurationException
	 *             if the given class is not compatible or valid as an
	 *             Application's configuration
	 * @throws IllegalStateException
	 *             if there is no room to print a line
	 */
	public void printHelp(PrintWriter pw, int width, String cmdLineSyntax,
			String header, int leftPad, int descPad, String footer,
			boolean autoUsage) throws InvalidConfigurationException {

		if (cmdLineSyntax != null && !"".equals(cmdLineSyntax.trim())) {
			delegate.printUsage(pw, width, cmdLineSyntax);
		}

		if ((header != null) && (header.trim().length() > 0)) {
			delegate.printWrapped(pw, width, header);
		}

		Set<Command> commands = Util.createCLICommands(cfgClasses);
		if (commands.size() > 0) {
			delegate.printWrapped(pw, width, "\ncommands:");
			for (Command command : commands) {
				printCommand(pw, width, command, leftPad, descPad, true);
			}
		}

		Options options = Util.createCLIOptions(cfgClasses);
		if (options.getOptions().size() > 0) {

			delegate.printWrapped(pw, width, "\noptions:");
			delegate.printWrapped(pw, width, "");
			delegate.printOptions(pw, width, options, leftPad, descPad);
		}

		if ((footer != null) && (footer.trim().length() > 0)) {
			delegate.printWrapped(pw, width, footer);
		}

	}

	/**
	 * Prints a help for the given <code>command</code> that includes 
	 * command's options and arguments.
	 * 
	 * @param pw
	 *            the writer to which the help will be written
	 * @param width
	 *            the number of characters to be displayed on each line
	 * @param command
	 *            the command to print help for
	 * @param leftPad
	 *            the number of characters of padding to be prefixed to each
	 *            line
	 * @param descPad
	 *            the number of characters of padding to be prefixed to each
	 *            description line
	 */
	public void printCommand(PrintWriter pw, int width, Command command,
			int leftPad, int descPad, boolean showOptions) {
		
		String name = "";
		if (command.fullName().length() > 0)
			name = command.fullName();
		if (command.shortName().length() > 0)
			name = (name.length() > 0) ? name + ", " + command.shortName() : command.shortName();
			
		String cmdSyntax = String.format(createPadding(leftPad) + " %-"+leftPad + descPad+"s%s",
				name, command.description());

		delegate.printWrapped(pw, width, cmdSyntax);
		Options options = Util.createCLIOptions(isIncCommonOptions(), command, cfgClasses);
		if (options.getOptions().size() > 0) {
			delegate.printOptions(pw, width, options, leftPad * 2 + 1, descPad);
			delegate.printWrapped(pw, 0, "");
		}
	}

	/**
	 * Print the help about exposed <code>applications</code> to the shell.
	 * 
	 * @param header
	 *            the header to print before of a list of applications
	 * @param exposedApplications
	 *            a list of application exposed in the shell
	 *            
	 * @throws IllegalStateException
	 *             if there is no room to print a line
	 */

	public void printHelp(String header, Set<Command> commands) {
		PrintWriter pw = new PrintWriter(System.out);

		printHelp(pw, org.apache.commons.cli.HelpFormatter.DEFAULT_WIDTH,
				header, commands,
				org.apache.commons.cli.HelpFormatter.DEFAULT_LEFT_PAD,
				org.apache.commons.cli.HelpFormatter.DEFAULT_DESC_PAD);
		pw.flush();
	}

	/**
	 * Print the help about exposed <code>applications</code> to the shell.
	 * 
	 * @param pw
	 *            the writer to which the help will be written
	 * @param width
	 *            the number of characters to be displayed on each line
	 * @param header
	 *            the header to print before of a list of applications
	 * @param exposedApplications
	 *            a list of application exposed in the shell
	 * @param leftPad
	 *            the number of characters of padding to be prefixed to each
	 *            line
	 * @param descPad
	 *            the number of characters of padding to be prefixed to each
	 *            description line
	 * @throws IllegalStateException
	 *             if there is no room to print a line
	 */
	public void printHelp(PrintWriter pw, int width, String header,
			Set<Command> commands, int leftPad, int descPad) {
		org.apache.commons.cli.HelpFormatter delegate = new org.apache.commons.cli.HelpFormatter();

		delegate.printWrapped(pw, width, header);

		for (Command c : commands) {

			printCommand(pw, width, c, leftPad, descPad, false);
		}
	}

	/**
	 * Return a String of padding of length <code>len</code>.
	 * 
	 * @param len
	 *            The length of the String of padding to create.
	 * 
	 * @return The String of padding
	 */
	public static String createPadding(int len) {
		StringBuffer sb = new StringBuffer(len);

		for (int i = 0; i < len; ++i) {
			sb.append(' ');
		}

		return sb.toString();
	}

	public boolean isConvertPropsToOptions() {
		return convertPropsToOptions;
	}

	public void setConvertPropsToOptions(boolean convertPropsToOptions) {
		this.convertPropsToOptions = convertPropsToOptions;
	}

	public boolean isIncCommonOptions() {
		return incCommonOptions;
	}

	public void setIncCommonOptions(boolean incCommonOptions) {
		this.incCommonOptions = incCommonOptions;
	}
}
